#include <number.h>
#include <log.h>

#define NO_DOT (-1)
typedef enum {
	OP_ADD, OP_SUB
}OP_ADD_SUB;

int calc_abs_compare(number a, number b);
static number _calc_free_list = NULL;
static int is_good(char *str);
static calc_int get_dot(char *str);
static calc_int get_str_length(char *str);
static void trim_str(char *src, char *des);

#define marc_adjust_op(a, b, c, op, carry) \
			do { \
				if (op == OP_ADD) { \
					c = a + b + carry; \
					if ( c >= BASE_MASK) { \
						c -= BASE_MASK;\
						carry = 1; \
					} else { \
						carry = 0; \
					} \
				} else { \
					c = a - b + carry; \
					if (c < 0) { \
						c += BASE_MASK; \
						carry = -1; \
					} else { \
						carry = 0; \
					} \
				} \
			} while(0)
/*
 * Remove '0' at the beginning of inter part and at the end of scale part
*/
void trim_str(char *str, char *des)
{
	char *ptr_begin = str;
	char *ptr_des = des;
	char *ptr_end = NULL; 
	if (*ptr_begin == '-') {
		*ptr_des = '-';
		ptr_des++;
		ptr_begin++;
	}

	while (*(ptr_begin + 1) != '.' && *(ptr_begin + 1) != 0) {
		if (*(ptr_begin) == '0')
			ptr_begin++;
		else
			break;
	}
	
	while (*ptr_begin != 0) {
		if (*ptr_begin != '0')
			ptr_end = ptr_des;

		*ptr_des++ = *ptr_begin++;
	}
	
	*(ptr_end + 1) = 0;
}

void print_num(number a)
{
	char *buf = NULL;
	int length = 0;
	
	number_to_str(a, buf, &length);
	if (length != 0)
		buf = (char *)malloc(length);

	printf("%s\n",number_to_str(a,buf,&length));
}

static int init_nbr(calc_int int_len, calc_int scale_len, number nbr)
{
	nbr->sign = PLUS;
	nbr->int_len = int_len;
	nbr->sca_len = scale_len;
	nbr->refs = 1;
	nbr->next = NULL;
	nbr->value = (calc_char	*)malloc(int_len + scale_len);
	if (nbr->value == NULL) 
		return CALC_E_NOT_ENOUGH_MEMORY;
	memset(nbr->value, 0, int_len + scale_len);
	return CALC_E_SUCCESS;
}

/*	if a > b , the return value is 1
	if a < b ,the return value is -1
	if a== b ,the return value is 0
*/
int calc_compare(number a, number b)
{
	if (a->sign != b->sign) {
		if (a->sign == PLUS)
			return 1;
		else
			return -1;
	} else {
		if (a->sign == PLUS) {
			return calc_abs_compare(a, b);
		} else {
			return -calc_abs_compare(a, b);
		}
	}
	return 0;
}

/*
	if |a| > |b|, the return value is 1
	if |a| < |b|, the return value is -1
	if |a| == |b|, the return value is 0
*/
int calc_abs_compare(number a, number b)
{
	calc_char * ptr_a_begin = a->value;
	calc_char *ptr_b_begin = b->value;
	
	if (a->int_len > b->int_len) {
		return 1;
	} else if (a->int_len < b->int_len) {
		return -1;
	} else {
		while (ptr_a_begin - a->value != a->int_len + a->sca_len + 1
		&& ptr_b_begin - b->value != b->int_len + b->sca_len + 1) {
			if (*ptr_a_begin > *ptr_b_begin){
				return 1;
			} else if (*ptr_a_begin < *ptr_b_begin) {
				return -1;
			} else {
				ptr_a_begin++;
				ptr_b_begin++;
			}
		}

		if (a->int_len + a->sca_len > b->int_len + b->sca_len)
			return 1;
		else if (a->int_len + a->sca_len < b->int_len + b->sca_len)
			return -1;
		else
			return 0;
	}
	
	return 0;
}

number new_nbr(calc_int int_len, calc_int scale_len)
{
	number tmp = NULL;
	
	if (int_len == 0)
		return NULL;

	if (_calc_free_list != NULL) {
		tmp = _calc_free_list;
		_calc_free_list = _calc_free_list->next;
	}
	else {
		tmp = malloc(sizeof(struct calc_num));
		if (tmp == NULL)
			return NULL;
	}
	
	if (init_nbr(int_len, scale_len, tmp) != CALC_E_SUCCESS) {
		free_nbr(tmp);
		return NULL;
	}
	
	return tmp;
}

int free_nbr(number nbr)
{
	if (nbr == NULL)
		return CALC_E_SUCCESS;

	nbr->refs--;
	if (nbr->refs == 0) {
		nbr->int_len = 0;
		nbr->sca_len = 0;
		if (nbr->value != NULL)
			free(nbr->value);

		nbr->next = _calc_free_list;
		_calc_free_list = nbr;
	}
	nbr = NULL;

	return CALC_E_SUCCESS;
}

/*	if op == OP_ADD , this function executes add operation
	if op == OP_SUB, this function executes sub operation
	base_add_sub function is used only when a is not smaller than b.
*/
static number base_add_sub(number a, number b, OP_ADD_SUB op)
{
	calc_int carry = 0;
	number c;
	calc_int c_index;
	calc_int c_int_len, c_sca_len;
	calc_int i;

	c_int_len = a->int_len + 1;
	c_sca_len = MAX(a->sca_len, b->sca_len);

	c = new_nbr(c_int_len, c_sca_len);
	if (c == NULL)
		return c;

	c_index = c_int_len + c_sca_len - 1; 
	/* copy from a to c */
	i = 0;
	while (i < a->int_len + a->sca_len ) {
		c->value[i+1] = a->value[i];
		i++;
	}
	
	/* if b->sca_len > a->sca_len */
	i = b->int_len + b->sca_len - 1;
	while (i - b->int_len + 1 > a->sca_len) {
		marc_adjust_op(0, b->value[i],c->value[c_index], op, carry);
		i--;
		c_index--;
	}
	
	c_index -= ABS(a->sca_len - (i - b->int_len + 1));
	while (i + 1 > 0) {
		marc_adjust_op(c->value[c_index], b->value[i],
					c->value[c_index], op, carry);
		i--;
		c_index--;
	}
	
	c->value[c_index] += carry;
	return c;
}

number base_sub(number a, number b)
{
	number result;
	if (a->sign == b->sign) {
		if (calc_abs_compare(a,b) > 0) {
			result = base_add_sub(a, b, OP_SUB);
			result->sign = PLUS;
		} else {
			result = base_add_sub(b, a, OP_SUB);
			result->sign = MINUS;
		}
	} else {
		if (calc_abs_compare(a,b) > 0) {
			result = base_add_sub(a, b, OP_ADD);
			result->sign = a->sign;
		} else {
			result = base_add_sub(b, a, OP_ADD);
			result->sign = a->sign;
		}
	}

	return result;
}

number base_add(number a, number b)
{
	number result;
	if (a->sign == b->sign) {
		if (calc_abs_compare(a,b) > 0)
			return base_add_sub(a, b, OP_ADD);
		else
			return base_add_sub(b, a, OP_ADD);
	} else {
		if (calc_abs_compare(a, b) > 0) {
			result = base_add_sub(a, b, OP_SUB);
			result->sign = a->sign;
		} else {
			result = base_add_sub(b, a, OP_SUB);
			result->sign = b->sign;
		}
	}
	return result;
}

static number _base_mul(number a, number b)
{
	number result;
	calc_char *ptr_a, *ptr_b, *ptr_res;
	calc_char *ptr_end_a, *ptr_end_b;
	int index, sum, res_len;
	int a_len, b_len;

	a_len = a->int_len + a->sca_len;
	b_len = b->int_len + b->sca_len;
	res_len = a_len + b_len + 1;

	result = new_nbr(res_len, 0);
	if (result == NULL)
		return NULL;
	result->int_len = a->int_len + b->int_len + 1;
	result->sca_len = a->sca_len + b->sca_len;

	ptr_end_a = a->value + a_len - 1;
	ptr_end_b = b->value + b_len - 1;
	ptr_res = result->value + result->int_len + result->sca_len - 1;
	sum = 0;

	for (index = 0; index < res_len - 1; index++) {
		ptr_a = ptr_end_a - MAX(0, index - b_len + 1);
		ptr_b = ptr_end_b - MIN(index, b_len - 1);

		while (ptr_a >= a->value && ptr_b <= ptr_end_b) {
			sum += *ptr_a-- * (*ptr_b++);
		}
		*ptr_res-- = sum % BASE_MASK;
		sum = sum / BASE_MASK;
	}
	*ptr_res = sum;
	return result;
}
number base_mul(number a, number b)
{
	number result = _base_mul(a, b);

	if (result != NULL) {
		if (a->sign != b->sign)
			result->sign = MINUS;
		else
			result->sign = PLUS;
	}

	return result;
}

static int is_good(char *str)
{
	return CALC_E_SUCCESS;
}

static calc_int get_dot(char *str)
{
	char *tmp = str;

	while (*tmp != 0) {
		if (*tmp == '.') {
			return tmp - str;
		}
		tmp++;
	}

	return NO_DOT;
}

static calc_int get_str_length(char *str)
{
	calc_int str_length = 0;
	while (str[str_length] != 0) {
		str_length++;
	}
	return str_length;
}

static void get_int_sca_len(char *str, calc_int *int_len, calc_int *sca_len)
{
	calc_int dot_pos = -1;
	calc_int int_len_tmp = 0;
	calc_int sca_len_tmp = 0;
	calc_int str_len = 0;

	str_len = get_str_length(str);
	dot_pos = get_dot(str);
	if (dot_pos == NO_DOT) {
		int_len_tmp = str_len;
		sca_len_tmp = 0;
	} else {
		int_len_tmp = dot_pos;
		sca_len_tmp = str_len - int_len_tmp - 1;
	}
	
	if (*str == '-') {
		int_len_tmp--;
	}
	*int_len = int_len_tmp;
	*sca_len = sca_len_tmp;
}

/*
 * convert num from number type to string type.
 * if the function return NULL, the buffer size is small.
 */
char *number_to_str(number num, char *buffer, size_t* length)
{
	calc_int buf_size = 1;
	calc_int i, j;
	
	i = 0;
	j = 0;

	if (num == NULL)
		return NULL;
	
	if (num->sign == MINUS)
		buf_size++;
	
	if (num->sca_len > 0)
		buf_size++;

	buf_size += num->int_len + num->sca_len;
	if (buffer == NULL || buf_size > *length) {
		*length = buf_size;
		return NULL;
	}
	
	if (num->sign == MINUS)
		buffer[i++] = '-';

	j = 0;
	while (j < num->int_len) {
		buffer[i++] = num->value[j++] + '0';
	}

	if (num->sca_len > 0)
		buffer[i++] = '.';

	while (j < num->int_len + num->sca_len) {
		buffer[i++] = num->value[j++] + '0';
	}
	
	buffer[i] = 0;
	return buffer;
}

number str_to_number(char *str_num)
{
	number num = NULL;
	char *str = NULL;
	calc_int int_len = 0;
	calc_int sca_len = 0;
	calc_int dot_pos = -1;
	calc_int index = 0;
	calc_int str_len = 0;
	calc_int num_index = 0;

	if (is_good(str) != CALC_E_SUCCESS) {
		return NULL;
	}
	
	str_len = get_str_length(str_num);
	str = (char *)malloc(str_len + 1);
	if (str == NULL)
		return NULL;
	trim_str(str_num, str);

	get_int_sca_len(str, &int_len, &sca_len);
	num = new_nbr(int_len, sca_len);
	if (num == NULL)
		goto end_proc;
	
	if (*str == '-') {
		num->sign = MINUS;
		index++;
	}

	dot_pos = get_dot(str);

	while (str[index] != 0) {
		if (dot_pos == NO_DOT) {
			num->value[num_index] = str[index] - '0';
		} else {
			if (index < dot_pos) {
				num->value[num_index] = str[index] - '0';
			} else if (index > dot_pos) {
				num->value[num_index - 1] = str[index] - '0';
			}
		}
		num_index++;
		index++;
	}

end_proc:
	if (str != NULL)
		free(str);
	return num;
}

int number_test(void)
{
	char * str="22";
	char * str2  =	"55";
	char buffer[1024] = {0};
	int length = 1024;

	number num2 = str_to_number(str2);
	number num = str_to_number(str);
	number result = NULL;

	print_num(num);
	print_num(num2);

	result =base_sub(num, num2);
	printf("%s\n",number_to_str(result,buffer,&length));
	return 0;
}
